package cz.proks.bank.manager;

import java.math.BigDecimal;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import cz.cvut.felk.support.aos.sw.clearingcenter.PaymentResult;
import cz.proks.bank.BankConstants;
import cz.proks.bank.client.ClearingServiceClient;
import cz.proks.bank.domain.Bank;
import cz.proks.bank.domain.Payment;
import cz.proks.bank.domain.Payment.State;
import cz.proks.bank.manager.exeception.BankRegisterException;
import cz.proks.bank.manager.exeception.PaymentException;
import cz.proks.bank.manager.exeception.TransferException;
import cz.proks.bank.service.clearingservice.FetchPaymentsFaultExceptionException;
import cz.proks.bank.service.clearingservice.RegisterFaultExceptionException;
import cz.proks.bank.service.clearingservice.SendPaymentsFaultExceptionException;
import cz.proks.bank.service.clearingservice.SetPaymentsResultFaultExceptionException;

@Service
public class ClearingManager {
	private final Logger log = Logger.getLogger(getClass());
	
	@Autowired
	private ClearingServiceClient clearingClient;
	
	@Autowired
	private BankManager bankManager;
	
	@Autowired
	private PaymentManager paymentManager;
	
	@Autowired
	private AccountManager accountManager;
	
	public String registerBank(String bankCode) throws BankRegisterException {
		log.info("registerBank():: Registering bank under bank code '" + bankCode + "'");
		
		try { 
			String bankKey = clearingClient.register(bankCode);
			
			log.info("registerBank():: Bank is registered! Bank identifier is '" + bankKey + "'");
			
			return bankKey;
		} catch (RegisterFaultExceptionException e) {
			throw new BankRegisterException("Bank '" + bankCode+ "' registration failed!", e);
		}		
	}	
	
	public void transferPayment(Payment payment) throws TransferException {		
		log.info("transgerMoney():: Start with payment " + payment);								
		
		try { 
			paymentManager.checkPayment(payment);
			
		} catch (PaymentException e) {
			throw new TransferException("Transfer failed because of payment inconsistence!", e);
		}		
		
		if (bankManager.isLocalBank(payment.getTargetBankCode()))
			throw new TransferException("Bank identified by " + payment.getTargetBankCode() + " code is local!");		
				
		doTransfer(payment);		
				
		log.info("transgerMoney():: Exit with payment " + payment);
	}	
	
	public List<Payment> fetchPayments() throws PaymentException {
		Bank bank = bankManager.getBank();
		
		try {
			List<cz.cvut.felk.support.aos.sw.clearingcenter.Payment> clearingPayments = clearingClient.fetchPayments(bank.getId());
			List<Payment> payments = new LinkedList<Payment>();
			
			if (clearingPayments != null)
				for (cz.cvut.felk.support.aos.sw.clearingcenter.Payment clearingPayment : clearingPayments) {
					payments.add(createPayment(clearingPayment));
				}
			
			return payments;
		} catch (FetchPaymentsFaultExceptionException e) {
			throw new PaymentException(e);
		}		
	}	
	
	public void setPaymentResult(Payment payment) throws PaymentException {
		if (payment == null)
			throw new PaymentException("No payment!");
		
		Bank bank = bankManager.getBank();
		
		try {
			List<cz.cvut.felk.support.aos.sw.clearingcenter.Payment> clearingPaymnets = new LinkedList<cz.cvut.felk.support.aos.sw.clearingcenter.Payment>();
			cz.cvut.felk.support.aos.sw.clearingcenter.Payment clearingPayment = createClearingPaymentWithResult(payment);
			
			clearingPaymnets.add(clearingPayment);
			
			clearingClient.setPaymentsResult(bank.getId(), clearingPaymnets);
		
		} catch (SetPaymentsResultFaultExceptionException e) {
			throw new PaymentException(e);
		}
	}
	
	private void doTransfer(Payment payment) throws TransferException {
		List<cz.cvut.felk.support.aos.sw.clearingcenter.Payment> clearingPayments = new LinkedList<cz.cvut.felk.support.aos.sw.clearingcenter.Payment>();
		cz.cvut.felk.support.aos.sw.clearingcenter.Payment clearingPayment = createClearingPayment(payment);
		Bank bank = bankManager.getBank();
				
		clearingPayments.add(clearingPayment);
		
		try {
			List<Long> paymentsIds = clearingClient.sendPayments(bank.getId(), clearingPayments);
			
			payment.setClearingPaymentId(paymentsIds.get(0));			
			
			payment.setPaymentState(State.WAITING_FOR_OTHER_BANK);
			
		} catch (SendPaymentsFaultExceptionException e) {
			if (payment != null)
				throw new TransferException("Cannot transfer money from " + payment.getSourceAccountId() + " account " +
											"to " + payment.getTargetAccountId() + " account identified by " + payment.getTargetBankCode() + " bank!"
											, e);
			else 
				throw new TransferException("Cannot transfer money to other bank!", e);			
		}
	}
	
	private cz.cvut.felk.support.aos.sw.clearingcenter.Payment createClearingPayment(Payment payment) {
		if (payment == null)
			return null;
		
		cz.cvut.felk.support.aos.sw.clearingcenter.Payment clearingPayment = new cz.cvut.felk.support.aos.sw.clearingcenter.Payment();
		
		clearingPayment.setAccountNumber(payment.getTargetAccountId());
		clearingPayment.setAmount(new BigDecimal(payment.getAmount()));
		clearingPayment.setBankCode(payment.getTargetBankCode());
		clearingPayment.setCurrency(payment.getCurrency());
		clearingPayment.setSenderAccountNumber(payment.getSourceAccountId());
		clearingPayment.setSenderBankCode(BankConstants.BANK_CODE);
		
		return clearingPayment;
	}
	
	private cz.cvut.felk.support.aos.sw.clearingcenter.Payment createClearingPaymentWithResult(Payment payment) throws PaymentException {
		if (payment == null)
			return null;
		
		cz.cvut.felk.support.aos.sw.clearingcenter.Payment clearingPayment = createClearingPayment(payment);
		clearingPayment.setPaymentResult(convertPaymentState(payment));
		
		return clearingPayment;
	}
	
	private Payment createPayment(cz.cvut.felk.support.aos.sw.clearingcenter.Payment clearingPayment) {
		if (clearingPayment == null)
			return null;
		
		Payment payment = new Payment();
		
		payment.setClearingPaymentId(clearingPayment.getId());
		payment.setSourceBankCode(clearingPayment.getSenderBankCode());
		payment.setSourceAccountId(clearingPayment.getSenderAccountNumber());
		payment.setTargetBankCode(clearingPayment.getBankCode());
		payment.setTargetAccountId(clearingPayment.getAccountNumber());
		payment.setAmount(clearingPayment.getAmount().longValue());
		payment.setCurrency(clearingPayment.getCurrency());
		payment.setDate(new Date());
		
		if (clearingPayment.getPaymentResult() != null)
			payment.setPaymentState(convertPaymentState(clearingPayment));
		
		return payment;
	}
	
	private State convertPaymentState(cz.cvut.felk.support.aos.sw.clearingcenter.Payment clearingPayment) {
		if (clearingPayment == null)
			return State.ERROR;
		
		switch (clearingPayment.getPaymentResult()) {
			case NO_SUCH_TARGET_ACCOUNT_ERROR:
			case NOTSET:
			case UNKNOWN_ERROR:
				return State.ERROR;
			
			case OK:
				return State.OK;
				
			case WAITING_FOR_TARGET_BANK_PROCESSING:
				return State.NEW;
				
			default:
				return State.ERROR;
		}
	}
	
	private PaymentResult convertPaymentState(Payment payment) throws PaymentException {
		if (payment == null)
			throw new PaymentException("No payment!");
		
		if (!accountManager.isLocalBankAccount(payment.getTargetAccountId(), payment.getTargetBankCode()))
			return PaymentResult.NO_SUCH_TARGET_ACCOUNT_ERROR;
		
		if (payment.getPaymentState() != State.OK)
			throw new PaymentException("Cannot set other clearing payment state than OK or NO_SUCH_TARGET_ACCOUNT!");
		
		return PaymentResult.OK;
	}
}
